/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const Services = require("Services");
import Rule from "devtools/client/inspector/rules/models/rule";
import UserProperties from "devtools/client/inspector/rules/models/user-properties";
import { NodeFront } from "protocol/thread/node";
import { RuleFront } from "protocol/thread/rule";
import { StyleFront } from "protocol/thread/style";
import { assert } from "protocol/utils";
import { UIStore } from "ui/actions";
import CSSProperties from "../../css-properties";
import RulesView from "../rules";
import TextProperty, { ComputedProperty } from "./text-property";

// const { promiseWarn } = require("devtools/client/inspector/shared/utils");

var NON_ASCII = "[^\\x00-\\x7F]";
var ESCAPE = "\\\\[^\n\r]";
var FIRST_CHAR = ["[_a-z]", NON_ASCII, ESCAPE].join("|");
var TRAILING_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
var IS_VARIABLE_TOKEN = new RegExp(`^--(${FIRST_CHAR})(${TRAILING_CHAR})*$`, "i");

function isCssVariable(input: string) {
  return !!input.match(IS_VARIABLE_TOKEN);
}

const PREF_INACTIVE_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";

/**
 * ElementStyle is responsible for the following:
 *   Keeps track of which properties are overridden.
 *   Maintains a list of Rule objects for a given element.
 */
export default class ElementStyle {
  element: NodeFront;
  ruleView: RulesView;
  store: UIStore;
  pageStyle: undefined;
  pseudoElements: string[];
  showUserAgentStyles: boolean;
  rules: Rule[] | null;
  cssProperties: typeof CSSProperties;
  variablesMap: Map<string, Map<string, string>>;
  destroyed?: boolean;

  private _unusedCssEnabled?: boolean;

  /**
   * @param  {NodeFront} element
   *         The element whose style we are viewing.
   * @param  {CssRuleView} ruleView
   *         The instance of the rule-view panel.
   * @param  {Object} store
   *         The ElementStyle can use this object to store metadata
   *         that might outlast the rule view, particularly the current
   *         set of disabled properties.
   * @param  {PageStyleFront} pageStyle
   *         Front for the page style actor that will be providing
   *         the style information.
   * @param  {Boolean} showUserAgentStyles
   *         Should user agent styles be inspected?
   */
  constructor(
    element: NodeFront,
    ruleView: RulesView,
    store: UIStore,
    pageStyle: undefined,
    showUserAgentStyles: boolean
  ) {
    this.element = element;
    this.ruleView = ruleView;
    this.store = store || {};
    this.pageStyle = pageStyle;
    this.pseudoElements = [];
    this.showUserAgentStyles = showUserAgentStyles;
    this.rules = [];
    this.cssProperties = this.ruleView.cssProperties;
    this.variablesMap = new Map<string, Map<string, string>>();

    // We don't want to overwrite this.store.userProperties so we only create it
    // if it doesn't already exist.
    if (!("userProperties" in this.store)) {
      this.store.userProperties = new UserProperties();
    }

    // if (!("disabled" in this.store)) {
    //   this.store.disabled = new WeakMap();
    // }

    // this.onRefresh = this.onRefresh.bind(this);

    // if (this.ruleView.isNewRulesView) {
    //   this.pageStyle.on("stylesheet-updated", this.onRefresh);
    //   this.ruleView.inspector.styleChangeTracker.on("style-changed", this.onRefresh);
    //   this.ruleView.selection.on("pseudoclass", this.onRefresh);
    // }
  }

  get unusedCssEnabled() {
    if (!this._unusedCssEnabled) {
      this._unusedCssEnabled = Services.prefs.getBoolPref(PREF_INACTIVE_CSS_ENABLED, false);
    }
    return this._unusedCssEnabled;
  }

  destroy() {
    if (this.destroyed) {
      return;
    }

    this.destroyed = true;
    this.pseudoElements = [];

    for (const rule of this.rules || []) {
      // if (rule.editor) {
      //   rule.editor.destroy();
      // }

      rule.destroy();
    }

    // if (this.ruleView.isNewRulesView) {
    //   this.pageStyle.off("stylesheet-updated", this.onRefresh);
    //   this.ruleView.inspector.styleChangeTracker.off("style-changed", this.onRefresh);
    //   this.ruleView.selection.off("pseudoclass", this.onRefresh);
    // }
  }

  /**
   * Called by the Rule object when it has been changed through the
   * setProperty* methods.
   */
  // _changed() {
  //   if (this.onChanged) {
  //     this.onChanged();
  //   }
  // }

  /**
   * Refresh the list of rules to be displayed for the active element.
   * Upon completion, this.rules[] will hold a list of Rule objects.
   *
   * Returns a promise that will be resolved when the elementStyle is
   * ready.
   */
  async populate() {
    this.rules = [];

    const applied = await this.element.getAppliedRules();
    if (applied === null) {
      this.rules = null;
      return;
    }

    // Show rules applied to pseudo-elements first.
    for (const { rule, pseudoElement } of applied) {
      if (pseudoElement) {
        this._maybeAddRule(rule, undefined, pseudoElement);
      }
    }

    // The inline rule has higher priority than applied rules.
    const inline = this.element.getInlineStyle();
    if (inline) {
      this._maybeAddRule(inline);
    }

    // Show rules applied directly to the element in priority order.
    for (const { rule, pseudoElement } of applied) {
      if (!pseudoElement) {
        this._maybeAddRule(rule);
      }
    }

    // Show relevant rules applied to parent elements.
    for (let elem = this.element.parentNode(); elem; elem = elem.parentNode()) {
      if (elem.nodeType == Node.ELEMENT_NODE) {
        const parentInline = elem.getInlineStyle();
        if (parentInline && parentInline.properties.length > 0) {
          this._maybeAddRule(parentInline, elem);
        }
        const parentApplied = await elem.getAppliedRules();
        if (parentApplied === null) {
          this.rules = null;
          return;
        }
        for (const { rule, pseudoElement } of parentApplied) {
          if (!pseudoElement) {
            this._maybeAddRule(rule, /* inherited */ elem);
          }
        }
      }
    }

    // Store a list of all pseudo-element types found in the matching rules.
    this.pseudoElements = this.rules.filter(r => r.pseudoElement).map(r => r.pseudoElement);

    // Mark overridden computed styles.
    this.onRuleUpdated();

    // if (this.ruleView.isNewRulesView) {
    //   this.subscribeRulesToLocationChange();
    // }
  }

  /**
   * Returns the Rule object of the given rule id.
   *
   * @param  {String|null} id
   *         The id of the Rule object.
   * @return {Rule|undefined} of the given rule id or undefined if it cannot be found.
   */
  getRule(id: string | null) {
    return id ? this.rules?.find(rule => rule.domRule.objectId() === id) : undefined;
  }

  /**
   * Get the font families in use by the element.
   *
   * Returns a promise that will be resolved to a list of CSS family
   * names. The list might have duplicates.
   */
  // getUsedFontFamilies() {
  //   return new Promise((resolve, reject) => {
  //     this.ruleView.styleWindow.requestIdleCallback(async () => {
  //       try {
  //         const fonts = await this.pageStyle.getUsedFontFaces(this.element, {
  //           includePreviews: false,
  //         });
  //         resolve(fonts.map(font => font.CSSFamilyName));
  //       } catch (e) {
  //         reject(e);
  //       }
  //     });
  //   });
  // }

  /**
   * Add a rule if it's one we care about. Filters out duplicates and
   * inherited styles with no inherited properties.
   *
   * @param  {RuleFront} ruleFront
   *         Rule to add.
   * @return {Boolean} true if we added the rule.
   */
  private _maybeAddRule(
    ruleFront: RuleFront | StyleFront,
    inherited?: NodeFront,
    pseudoElement?: string
  ) {
    if (ruleFront.isSystem) {
      // We currently don't display any user agent styles.
      // See https://github.com/RecordReplay/devtools/issues/546
      return false;
    }

    this.rules?.push(new Rule(this, { rule: ruleFront, inherited, pseudoElement }));
    return true;
  }

  /**
   * Calls updateDeclarations with all supported pseudo elements
   */
  onRuleUpdated() {
    this.updateDeclarations();

    // Update declarations for matching rules for pseudo-elements.
    for (const pseudo of this.pseudoElements) {
      this.updateDeclarations(pseudo);
    }
  }

  /**
   * Go over all CSS rules matching the selected element and mark the CSS declarations
   * (aka TextProperty instances) with an `overridden` Boolean flag if an earlier or
   * higher priority declaration overrides it. Rules are already ordered by specificity.
   *
   * If a pseudo-element type is passed (ex: ::before, ::first-line, etc),
   * restrict the operation only to declarations in rules matching that pseudo-element.
   *
   * At the end, update the declaration's view (TextPropertyEditor instance) so it relects
   * the latest state. Use this opportunity to also trigger checks for the "inactive"
   * state of the declaration (whether it has effect or not).
   *
   * @param  {String} pseudo
   *         Optional pseudo-element for which to restrict marking CSS declarations as
   *         overridden.
   */
  updateDeclarations(pseudo = "") {
    // Gather all text properties applicable to the selected element or pseudo-element.
    const textProps = this._getDeclarations(pseudo);
    // Gather all the computed properties applied by those text properties.
    let computedProps: ComputedProperty[] = [];
    for (const textProp of textProps) {
      assert(textProp.computed);
      computedProps = computedProps.concat(textProp.computed);
    }

    // CSS Variables inherits from the normal element in case of pseudo element.
    const variables = new Map<string, string>(pseudo ? this.variablesMap.get("") || [] : []);

    // Walk over the computed properties. As we see a property name
    // for the first time, mark that property's name as taken by this
    // property.
    //
    // If we come across a property whose name is already taken, check
    // its priority against the property that was found first:
    //
    //   If the new property is a higher priority, mark the old
    //   property overridden and mark the property name as taken by
    //   the new property.
    //
    //   If the new property is a lower or equal priority, mark it as
    //   overridden.
    //
    // _overriddenDirty will be set on each prop, indicating whether its
    // dirty status changed during this pass.
    const taken: Record<string, ComputedProperty> = {};
    for (const computedProp of computedProps) {
      const earlier = taken[computedProp.name];

      // Prevent -webkit-gradient from being selected after unchecking
      // linear-gradient in this case:
      //  -moz-linear-gradient: ...;
      //  -webkit-linear-gradient: ...;
      //  linear-gradient: ...;
      if (!computedProp.textProp.isValid()) {
        computedProp.overridden = true;
        continue;
      }

      let overridden;
      if (
        earlier &&
        computedProp.priority === "important" &&
        earlier.priority !== "important" &&
        (earlier.textProp.rule.inherited || !computedProp.textProp.rule.inherited)
      ) {
        // New property is higher priority. Mark the earlier property
        // overridden (which will reverse its dirty state).
        earlier._overriddenDirty = !earlier._overriddenDirty;
        earlier.overridden = true;
        overridden = false;
      } else {
        overridden = !!earlier;
      }

      computedProp._overriddenDirty = !!computedProp.overridden !== overridden;
      computedProp.overridden = overridden;

      if (!computedProp.overridden && computedProp.textProp.enabled) {
        taken[computedProp.name] = computedProp;

        if (isCssVariable(computedProp.name)) {
          variables.set(computedProp.name, computedProp.value);
        }
      }
    }

    // Find the CSS variables that have been updated.
    const previousVariablesMap = new Map<string, string>(this.variablesMap.get(pseudo) || []);
    const changedVariableNamesSet = new Set<string>(
      [...variables.keys(), ...previousVariablesMap.keys()].filter(
        k => variables.get(k) !== previousVariablesMap.get(k)
      )
    );

    this.variablesMap.set(pseudo, variables);

    // For each TextProperty, mark it overridden if all of its computed
    // properties are marked overridden. Update the text property's associated
    // editor, if any. This will clear the _overriddenDirty state on all
    // computed properties. For each editor we also show or hide the inactive
    // CSS icon as needed.
    for (const textProp of textProps) {
      // _updatePropertyOverridden will return true if the
      // overridden state has changed for the text property.
      // _hasUpdatedCSSVariable will return true if the declaration contains any
      // of the updated CSS variable names.
      // if (
      //   this._updatePropertyOverridden(textProp) ||
      //   this._hasUpdatedCSSVariable(textProp, changedVariableNamesSet)
      // ) {
      //   textProp.updateEditor();
      // }
      this._updatePropertyOverridden(textProp);

      // For each editor show or hide the inactive CSS icon as needed.
      // if (textProp.editor && this.unusedCssEnabled) {
      //   textProp.editor.updatePropertyState();
      // }
    }
  }

  /**
   * Returns true if the given declaration's property value contains a CSS variable
   * matching any of the updated CSS variable names.
   *
   * @param {TextProperty} declaration
   *        A TextProperty of a rule.
   * @param {Set<String>} variableNamesSet
   *        A Set of CSS variable names that have been updated.
   */
  private _hasUpdatedCSSVariable(declaration: TextProperty, variableNamesSet: Set<string>) {
    for (const variableName of variableNamesSet) {
      if (declaration.hasCSSVariable(variableName)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Helper for |this.updateDeclarations()| to mark CSS declarations as overridden.
   *
   * Returns an array of CSS declarations (aka TextProperty instances) from all rules
   * applicable to the selected element ordered from more- to less-specific.
   *
   * If a pseudo-element type is given, restrict the result only to declarations
   * applicable to that pseudo-element.
   *
   * NOTE: this method skips CSS declarations in @keyframes rules because a number of
   * criteria such as time and animation delay need to be checked in order to determine
   * if the property is overridden at runtime.
   *
   * @param  {String} pseudo
   *         Optional pseudo-element for which to restrict marking CSS declarations as
   *         overridden. If omitted, only declarations for regular style rules are
   *         returned (no pseudo-element style rules).
   *
   * @return {Array}
   *         Array of TextProperty instances.
   */
  private _getDeclarations(pseudo = "") {
    const textProps: TextProperty[] = [];

    for (const rule of this.rules || []) {
      // Skip @keyframes rules
      // if (rule.keyframes) {
      //   continue;
      // }

      // Style rules must be considered only when they have selectors that match the node.
      // When renaming a selector, the unmatched rule lingers in the Rule view, but it no
      // longer matches the node. This strict check avoids accidentally causing
      // declarations to be overridden in the remaining matching rules.
      const isStyleRule = rule.pseudoElement === ""; // && rule.matchedSelectors.length > 0;

      // Style rules for pseudo-elements must always be considered, regardless if their
      // selector matches the node. As a convenience, declarations in rules for
      // pseudo-elements show up in a separate Pseudo-elements accordion when selecting
      // the host node (instead of the pseudo-element node directly, which is sometimes
      // impossible, for example with ::selection or ::first-line).
      // Loosening the strict check on matched selectors ensures these declarations
      // participate in the algorithm below to mark them as overridden.
      const isPseudoElementRule = rule.pseudoElement !== "" && rule.pseudoElement === pseudo;

      const isElementStyle = !rule.domRule.isRule();

      const filterCondition = pseudo === "" ? isStyleRule || isElementStyle : isPseudoElementRule;

      // Collect all relevant CSS declarations (aka TextProperty instances).
      if (filterCondition) {
        for (const textProp of rule.textProps.slice(0).reverse()) {
          if (textProp.enabled) {
            textProps.push(textProp);
          }
        }
      }
    }

    return textProps;
  }

  /**
   * Adds a new declaration to the rule.
   *
   * @param  {String} ruleId
   *         The id of the Rule to be modified.
   * @param  {String} value
   *         The new declaration value.
   */
  // addNewDeclaration(ruleId, value) {
  //   const rule = this.getRule(ruleId);
  //   if (!rule) {
  //     return;
  //   }

  //   const declarationsToAdd = parseNamedDeclarations(this.cssProperties.isKnown, value, true);
  //   if (!declarationsToAdd.length) {
  //     return;
  //   }

  //   this._addMultipleDeclarations(rule, declarationsToAdd);
  // }

  /**
   * Adds a new rule. The rules view is updated from a "stylesheet-updated" event
   * emitted the PageStyleActor as a result of the rule being inserted into the
   * the stylesheet.
   */
  // async addNewRule() {
  //   await this.pageStyle.addNewRule(this.element, this.element.pseudoClassLocks);
  // }

  /**
   * Given the id of the rule and the new declaration name, modifies the existing
   * declaration name to the new given value.
   *
   * @param  {String} ruleId
   *         The Rule id of the given CSS declaration.
   * @param  {String} declarationId
   *         The TextProperty id for the CSS declaration.
   * @param  {String} name
   *         The new declaration name.
   */
  // async modifyDeclarationName(ruleId, declarationId, name) {
  //   const rule = this.getRule(ruleId);
  //   if (!rule) {
  //     return;
  //   }

  //   const declaration = rule.getDeclaration(declarationId);
  //   if (!declaration || declaration.name === name) {
  //     return;
  //   }

  //   // Adding multiple rules inside of name field overwrites the current
  //   // property with the first, then adds any more onto the property list.
  //   const declarations = parseDeclarations(this.cssProperties.isKnown, name);
  //   if (!declarations.length) {
  //     return;
  //   }

  //   await declaration.setName(declarations[0].name);

  //   if (!declaration.enabled) {
  //     await declaration.setEnabled(true);
  //   }
  // }

  /**
   * Helper function to addNewDeclaration() and modifyDeclarationValue() for
   * adding multiple declarations to a rule.
   *
   * @param  {Rule} rule
   *         The Rule object to write new declarations to.
   * @param  {Array<Object>} declarationsToAdd
   *         An array of object containg the parsed declaration data to be added.
   * @param  {TextProperty|null} siblingDeclaration
   *         Optional declaration next to which the new declaration will be added.
   */
  // _addMultipleDeclarations(rule, declarationsToAdd, siblingDeclaration = null) {
  //   for (const { commentOffsets, name, value, priority } of declarationsToAdd) {
  //     const isCommented = Boolean(commentOffsets);
  //     const enabled = !isCommented;
  //     siblingDeclaration = rule.createProperty(name, value, priority, enabled, siblingDeclaration);
  //   }
  // }

  /**
   * Parse a value string and break it into pieces, starting with the
   * first value, and into an array of additional declarations (if any).
   *
   * Example: Calling with "red; width: 100px" would return
   * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
   *
   * @param  {String} value
   *         The string to parse.
   * @return {Object} An object with the following properties:
   *         firstValue: A string containing a simple value, like
   *                     "red" or "100px!important"
   *         declarationsToAdd: An array with additional declarations, following the
   *                            parseDeclarations format of { name, value, priority }
   */
  // _getValueAndExtraProperties(value: string) {
  //   // The inplace editor will prevent manual typing of multiple declarations,
  //   // but we need to deal with the case during a paste event.
  //   // Adding multiple declarations inside of value editor sets value with the
  //   // first, then adds any more onto the declaration list (below this declarations).
  //   let firstValue = value;
  //   let declarationsToAdd = [];

  //   const declarations = parseDeclarations(this.cssProperties.isKnown, value);

  //   // Check to see if the input string can be parsed as multiple declarations
  //   if (declarations.length) {
  //     // Get the first property value (if any), and any remaining
  //     // declarations (if any)
  //     if (!declarations[0].name && declarations[0].value) {
  //       firstValue = declarations[0].value;
  //       declarationsToAdd = declarations.slice(1);
  //     } else if (declarations[0].name && declarations[0].value) {
  //       // In some cases, the value could be a property:value pair
  //       // itself.  Join them as one value string and append
  //       // potentially following declarations
  //       firstValue = declarations[0].name + ": " + declarations[0].value;
  //       declarationsToAdd = declarations.slice(1);
  //     }
  //   }

  //   return {
  //     declarationsToAdd,
  //     firstValue,
  //   };
  // }

  /**
   * Given the id of the rule and the new declaration value, modifies the existing
   * declaration value to the new given value.
   *
   * @param  {String} ruleId
   *         The Rule id of the given CSS declaration.
   * @param  {String} declarationId
   *         The TextProperty id for the CSS declaration.
   * @param  {String} value
   *         The new declaration value.
   */
  // async modifyDeclarationValue(ruleId, declarationId, value) {
  //   const rule = this.getRule(ruleId);
  //   if (!rule) {
  //     return;
  //   }

  //   const declaration = rule.getDeclaration(declarationId);
  //   if (!declaration) {
  //     return;
  //   }

  //   const { declarationsToAdd, firstValue } = this._getValueAndExtraProperties(value);
  //   const parsedValue = parseSingleValue(this.cssProperties.isKnown, firstValue);

  //   if (
  //     !declarationsToAdd.length &&
  //     declaration.value === parsedValue.value &&
  //     declaration.priority === parsedValue.priority
  //   ) {
  //     return;
  //   }

  //   // First, set this declaration value (common case, only modified a property)
  //   await declaration.setValue(parsedValue.value, parsedValue.priority);

  //   if (!declaration.enabled) {
  //     await declaration.setEnabled(true);
  //   }

  //   this._addMultipleDeclarations(rule, declarationsToAdd, declaration);
  // }

  /**
   * Modifies the existing rule's selector to the new given value.
   *
   * @param  {String} ruleId
   *         The id of the Rule to be modified.
   * @param  {String} selector
   *         The new selector value.
   */
  // async modifySelector(ruleId, selector) {
  //   try {
  //     const rule = this.getRule(ruleId);
  //     if (!rule) {
  //       return;
  //     }

  //     const response = await rule.domRule.modifySelector(this.element, selector);
  //     const { ruleProps, isMatching } = response;

  //     if (!ruleProps) {
  //       // Notify for changes, even when nothing changes, just to allow tests
  //       // being able to track end of this request.
  //       this.ruleView.emit("ruleview-invalid-selector");
  //       return;
  //     }

  //     const newRule = new Rule(this, {
  //       ...ruleProps,
  //       isUnmatched: !isMatching,
  //     });

  //     // Recompute the list of applied styles because editing a
  //     // selector might cause this rule's position to change.
  //     const appliedStyles = await this.pageStyle.getApplied(this.element, {
  //       inherited: true,
  //       matchedSelectors: true,
  //       filter: this.showUserAgentStyles ? "ua" : undefined,
  //     });
  //     const newIndex = appliedStyles.findIndex(r => r.rule == ruleProps.rule);
  //     const oldIndex = this.rules.indexOf(rule);

  //     // Remove the old rule and insert the new rule according to where it appears
  //     // in the list of applied styles.
  //     this.rules.splice(oldIndex, 1);
  //     // If the selector no longer matches, then we leave the rule in
  //     // the same relative position.
  //     this.rules.splice(newIndex === -1 ? oldIndex : newIndex, 0, newRule);

  //     // Recompute, mark and update the UI for any properties that are
  //     // overridden or contain inactive CSS according to the new list of rules.
  //     this.onRuleUpdated();

  //     // In order to keep the new rule in place of the old in the rules view, we need
  //     // to remove the rule again if the rule was inserted to its new index according
  //     // to the list of applied styles.
  //     // Note: you might think we would replicate the list-modification logic above,
  //     // but that is complicated due to the way the UI installs pseudo-element rules
  //     // and the like.
  //     if (newIndex !== -1) {
  //       this.rules.splice(newIndex, 1);
  //       this.rules.splice(oldIndex, 0, newRule);
  //     }
  //     this._changed();
  //   } catch (e) {
  //     console.error(e);
  //   }
  // }

  /**
   * Subscribes all the rules to location changes.
   */
  // subscribeRulesToLocationChange() {
  //   for (const rule of this.rules) {
  //     rule.subscribeToLocationChange();
  //   }
  // }

  /**
   * Toggles the enabled state of the given CSS declaration.
   *
   * @param  {String} ruleId
   *         The Rule id of the given CSS declaration.
   * @param  {String} declarationId
   *         The TextProperty id for the CSS declaration.
   */
  // toggleDeclaration(ruleId, declarationId) {
  //   const rule = this.getRule(ruleId);
  //   if (!rule) {
  //     return;
  //   }

  //   const declaration = rule.getDeclaration(declarationId);
  //   if (!declaration) {
  //     return;
  //   }

  //   declaration.setEnabled(!declaration.enabled);
  // }

  /**
   * Mark a given TextProperty as overridden or not depending on the
   * state of its computed properties. Clears the _overriddenDirty state
   * on all computed properties.
   *
   * @param  {TextProperty} prop
   *         The text property to update.
   * @return {Boolean} true if the TextProperty's overridden state (or any of
   *         its computed properties overridden state) changed.
   */
  _updatePropertyOverridden(prop: TextProperty) {
    let overridden = true;
    let dirty = false;

    assert(prop.computed);
    for (const computedProp of prop.computed) {
      if (!computedProp.overridden) {
        overridden = false;
      }

      dirty = computedProp._overriddenDirty || dirty;
      delete computedProp._overriddenDirty;
    }

    dirty = !!prop.overridden !== overridden || dirty;
    prop.overridden = overridden;
    return dirty;
  }

  /**
   * Returns the current value of a CSS variable; or null if the
   * variable is not defined.
   *
   * @param  {String} name
   *         The name of the variable.
   * @param  {String} pseudo
   *         The pseudo-element name of the rule.
   * @return {String} the variable's value or null if the variable is
   *         not defined.
   */
  getVariable(name: string, pseudo = "") {
    const variables = this.variablesMap.get(pseudo);
    return variables ? variables.get(name) : null;
  }

  /**
   * Handler for "stylesheet-updated", "style-changed" and "pseudoclass" events.
   * Refreshes the list of rules on the page.
   */
  // async onRefresh() {
  //   // Repopulate the element style once the current modifications are done.
  //   const promises = [];
  //   for (const rule of this.rules) {
  //     if (rule._applyingModifications) {
  //       promises.push(rule._applyingModifications);
  //     }
  //   }

  //   await Promise.all(promises);
  //   await this.populate();
  //   this._changed();
  // }
}
